package interpreter;

import json.val.*;
import token.*;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;

public class Parser {
    private static class Pair {
        StringVal key;
        JsonVal val;

        public Pair(StringVal key, JsonVal val) {
            this.key = key;
            this.val = val;
        }
    }

    private final Lexer lexer;
    private Token currentToken;

    public Parser(Lexer lexer) throws Exception {
        this.lexer = lexer;
        this.currentToken = lexer.getNextToken();
    }

    private void error() throws Exception {
        throw new Exception("Invalid syntax");
    }

    private void eat(Class<?> clazz) throws Exception {
        if (currentToken.getClass() == clazz) {
            currentToken = lexer.getNextToken();
        } else {
            this.error();
        }
    }

    boolean sameClass(Class<?> clazz1, Class<?> clazz2) {
        return clazz1 == clazz2;
    }

    float getFloat(int part1, int part2) {
        if (part2 == -1)
            return part1;
        else {
            return Float.parseFloat(part1 + "." + part2);
        }
    }

    private Pair assignment() throws Exception {
        // assignment: stringVal ASSIGN value
        StringVal left = (StringVal) stringVal();
        eat(TK_Assign.class);
        JsonVal right = value();
        return new Pair(left, right);
    }

    private JsonVal value() throws Exception {
        /* value: booleanVal
                | numVal
                | stringVal
                | objectVal
                | arrayVal
        * */
        JsonVal res = null;
        Class<?> currentTokenClass = currentToken.getClass();
        if (sameClass(currentTokenClass, TK_Boolean.class)) {
            res = booleanVal();
        } else if (sameClass(currentTokenClass, TK_Integer.class)) {
            res = numVal();
        } else if (sameClass(currentTokenClass, TK_Quotation.class)) {
            res = stringVal();
        } else if (sameClass(currentTokenClass, TK_Lbrace.class)) {
            res = objectVal();
        } else if (sameClass(currentTokenClass, TK_Lbracket.class)) {
            res = arrayVal();
        } else {
            error();
        }
        return res;
    }

    private JsonVal booleanVal() throws Exception {
        // booleanVal: BOOLEAN
        Token token = currentToken;
        eat(TK_Boolean.class);
        return new BoolVal((Boolean) token.value);
    }

    private JsonVal numVal() throws Exception {
        // numVal: INTEGER (DOT INTEGER)
        Token token = currentToken;
        eat(TK_Integer.class);
        int part1 = (int) token.value;
        int part2 = -1;
        if (sameClass(currentToken.getClass(), TK_Dot.class)) {
            eat(TK_Dot.class);
            token = currentToken;
            eat(TK_Integer.class);
            part2 = (int) token.value;
        }
        float res = getFloat(part1, part2);
        return new NumVal(res);
    }

    private JsonVal stringVal() throws Exception {
        // stringVal: QUO  STRING  QUO
        eat(TK_Quotation.class);
        Token token = currentToken;
        eat(TK_String.class);
        eat(TK_Quotation.class);
        return new StringVal((String) token.value);
    }

    private JsonVal objectVal() throws Exception {
        // objectVal: LBRACE assignmentList RBRACE
        // { "a":10... }
        eat(TK_Lbrace.class);
        ObjectVal objectVal = new ObjectVal(assignmentList());
        eat(TK_Rbrace.class);
        return objectVal;
    }

    private JsonVal arrayVal() throws Exception {
        // arrayVal: LBRACKET valueList RBRACKET
        // [1,2,3...]
        eat(TK_Lbracket.class);
        ArrayVal arrayVal = new ArrayVal(valueList());
        eat(TK_Rbracket.class);
        return arrayVal;
    }

    private Map<StringVal, JsonVal> assignmentList() throws Exception {
        // assignmentList: assignment (COMMA assignment)*
        Map<StringVal, JsonVal> map = new HashMap<>();
        Pair pair = assignment();
        map.put(pair.key, pair.val);
        while (sameClass(TK_Comma.class, currentToken.getClass())) {
            eat(TK_Comma.class);
            pair = assignment();
            map.put(pair.key, pair.val);
        }
        return map;
    }

    private ArrayList<JsonVal> valueList() throws Exception {
        // valueList: value (COMMA value)*
        // true, false, "name"...
        ArrayList<JsonVal> valueList = new ArrayList<>();
        JsonVal value = value();
        valueList.add(value);
        while (sameClass(TK_Comma.class, currentToken.getClass())) {
            eat(TK_Comma.class);
            value = value();
            valueList.add(value);
        }
        return valueList;
    }

    public JsonVal parse() throws Exception {
        return value();
    }
}
